﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using TeF.Runner.Common.Extensions;
using TeF.Runner.Extensions;

namespace TeF.Runner.Services
{
    internal class TestFinder : ITestFinder
    {
        #region Public Methods

        public TestAssembly GetTestsInAssembly(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");

            var isAssemblyIgnored = assembly.HasAttribute<TestIgnoreAttribute>();
            var testAssembly = new TestAssembly(assembly, isAssemblyIgnored);

            var testNamespaces = GetTestNamespaces(testAssembly);
            testNamespaces.DoForEach(testAssembly.AddChild);

            return testAssembly;
        }

        public Test[] GetTestsToRun(ITestEntity[] testEntities)
        {
            if (testEntities == null)
                throw new ArgumentNullException("testEntities");
            if (testEntities.Any(e => e == null))
                throw new ArgumentException("testEntities");

            var resultTests = new HashSet<Test>();

            foreach (var testEntity in testEntities)
            {
                if (testEntity.State == TestState.Skipped)
                    continue;

                var testAssembly = testEntity as TestAssembly;
                if (testAssembly != null)
                {
                    var tests = testAssembly.GetTestsToRun();
                    MergeTestsToRun(resultTests, tests);
                    continue;
                }

                var testNamespace = testEntity as TestNamespace;
                if (testNamespace != null)
                {
                    var tests = testNamespace.GetTestsToRun();
                    MergeTestsToRun(resultTests, tests);
                    continue;
                }

                var testContainer = testEntity as TestContainer;
                if (testContainer != null)
                {
                    var tests = testContainer.GetTestsToRun();
                    MergeTestsToRun(resultTests, tests);
                    continue;
                }

                MergeTestsToRun(resultTests, new[] { (Test)testEntity });
            }

            return resultTests.ToArray();
        }

        #endregion


        #region Private Methods

        private static void MergeTestsToRun(HashSet<Test> resultTests, IEnumerable<Test> tests)
        {
            if (resultTests == null)
                throw new ArgumentNullException("resultTests");
            if (tests == null)
                throw new ArgumentNullException("tests");

            foreach (var test in tests)
            {
                if (!resultTests.Contains(test))
                    resultTests.Add(test);
            }
        }

        private static IEnumerable<TestNamespace> GetTestNamespaces(TestAssembly testAssembly)
        {
            var testTypes = testAssembly.Assembly.GetTestTypes<TestContainerAttribute>().ToArray();
            
            return 
                testTypes
                    .GroupBy(t => t.Namespace)
                    .OrderBy(t => t.Key)
                    .Select(
                        g =>
                        {
                            var testNamespace = new TestNamespace(g.Key, testAssembly.Ignored, testAssembly);

                            var testContainers = g.Select(t => CreateTestContainer(t, testNamespace));
                            testContainers.DoForEach(testNamespace.AddChild);

                            return testNamespace;
                        });
        }

        private static TestContainer CreateTestContainer(Type type, TestNamespace testNamespace)
        {
            var isContainerIgnored = testNamespace.Ignored || type.HasAttribute<TestIgnoreAttribute>();
            var testContainerSetUp = CreateTestContainerSetUp(type);
            var testContainerTearDown = CreateTestContainerTearDown(type);

            var testContainer = new TestContainer(type, isContainerIgnored, testNamespace, testContainerSetUp, testContainerTearDown);

            var testSetUp = CreateTestSetUp(type);
            var testTearDown = CreateTestTearDown(type);
            var testMethods = type.GetTestMethods<TestAttribute>().ToArray();

            var tests = testMethods.Select(m => CreateTest(m, testContainer, testSetUp, testTearDown));
            tests.DoForEach(testContainer.AddChild);

            return testContainer;
        }

        private static TestContainerSetUp CreateTestContainerSetUp(Type testType)
        {
            var testContainerSetUpMethods = testType.GetTestMethods<TestContainerSetUpAttribute>().ToArray();
            if (testContainerSetUpMethods.Length > 1)
                throw new TestFinderException(@"Test container ""{0}"" has more than one container's setup methods.", testType);
            
            var testContainerSetUpMethod = testContainerSetUpMethods.SingleOrDefault();
            
            if (testContainerSetUpMethod != null)
            {
                var isIgnored = testContainerSetUpMethod.HasAttribute<TestIgnoreAttribute>();

                return new TestContainerSetUp(testContainerSetUpMethod, isIgnored);
            }

            return null;
        }

        private static TestContainerTearDown CreateTestContainerTearDown(Type testType)
        {
            var testContainerTearDownMethods = testType.GetTestMethods<TestContainerTearDownAttribute>().ToArray();
            if (testContainerTearDownMethods.Length > 1)
                throw new TestFinderException(@"Test container ""{0}"" has more than one container's teardown methods.", testType);
            
            var testContainerTearDownMethod = testContainerTearDownMethods.SingleOrDefault();

            if (testContainerTearDownMethod != null)
            {
                var isIgnored = testContainerTearDownMethod.HasAttribute<TestIgnoreAttribute>();

                return new TestContainerTearDown(testContainerTearDownMethod, isIgnored);
            }

            return null;
        }

        private static TestSetUp CreateTestSetUp(Type testType)
        {
            var testSetUpMethods = testType.GetTestMethods<TestSetUpAttribute>().ToArray();
            if (testSetUpMethods.Length > 1)
                throw new TestFinderException(@"Test container ""{0}"" has more than one test's setup methods.", testType);

            var testSetUpMethod = testSetUpMethods.SingleOrDefault();

            if (testSetUpMethod != null)
            {
                var isIgnored = testSetUpMethod.HasAttribute<TestIgnoreAttribute>();

                return new TestSetUp(testSetUpMethod, isIgnored);
            }

            return null;
        }

        private static TestTearDown CreateTestTearDown(Type testType)
        {
            var testTearDownMethods = testType.GetTestMethods<TestTearDownAttribute>().ToArray();
            if (testTearDownMethods.Length > 1)
                throw new TestFinderException(@"Test container ""{0}"" has more than one test's teardown methods.", testType);

            var testTearDownMethod = testTearDownMethods.SingleOrDefault();

            if (testTearDownMethod != null)
            {
                var isIgnored = testTearDownMethod.HasAttribute<TestIgnoreAttribute>();

                return new TestTearDown(testTearDownMethod, isIgnored);
            }

            return null;
        }
        
        private static Test CreateTest(MethodInfo testMethod, TestContainer testContainer, TestSetUp setUp, TestTearDown tearDown)
        {
            var isTestIgnored = testContainer.Ignored || testMethod.HasAttribute<TestIgnoreAttribute>();

            return new Test(testMethod, isTestIgnored, testContainer, setUp, tearDown);
        }

        #endregion
    }
}
